package rip.core;

import com.mongodb.DB;
import org.apache.commons.codec.binary.Base64;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ObjectNode;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Query;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Properties;

public abstract class RIPService {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    private EntityManagerFactory emf;
    private EntityManager em;
    private OutputStream outputStream;
    private boolean testMode = false;
    private JsonNode output;
    private Object input;
    private ObjectMapper mapper;
    private DB db;
    private boolean inlineCall;
    private Properties envProperties;

    void setEnvProperties(Properties envProperties) {
        this.envProperties = envProperties;
    }

    protected String env(String key) {
        return envProperties.getProperty(key);
    }

    protected EntityManager jpa() {
        if (em == null)
            em = emf.createEntityManager();

        return em;
    }

    protected <T> T jpaQuery(Class<T> type, String queryString, JpaParam... params) {
        Query q = jpa().createQuery(queryString);

        for (int i = 0; i < params.length; i++) {
            JpaParam param = params[i];
            q.setParameter(param.name, param.value);
        }

        List<T> results = q.getResultList();

        if (results.size() == 0)
            return null;

        if (results.size() > 1)
            throw new IllegalStateException("More than one result found for query " + queryString);

        return results.get(0);
    }

    protected List jpaListQuery(String queryString, JpaParam... params) {
        Query q = jpa().createQuery(queryString);

        for (int i = 0; i < params.length; i++) {
            JpaParam param = params[i];
            q.setParameter(param.name, param.value);
        }

        return q.getResultList();
    }

    protected boolean jpaExists(String queryString, JpaParam... params) {
        Query q = jpa().createQuery(queryString);

        for (int i = 0; i < params.length; i++) {
            JpaParam param = params[i];
            q.setParameter(param.name, param.value);
        }

        return q.getResultList().size() > 0;
    }

    protected JpaParam qp(String name, Object value) {
        return new JpaParam(name, value);
    }

    public void setDb(DB db) {
        this.db = db;
    }

    protected DB mongo()  {
        return db;
    }

    void setEmf(EntityManagerFactory emf) {
        this.emf = emf;
    }

    void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    void setResponse(HttpServletResponse response) {
        this.response = response;
    }

    void setTestMode(boolean mode) {
        this.testMode = mode;
    }

    protected boolean isTestMode() {
        return testMode;
    }

    JsonNode getOutput() {
        return output;
    }

    protected void call(String methodName, Object message) {
        input = message;
        execute(methodName);
    }

    public void execute(String methodName) {
        boolean transactional = false;

        if (methodName == null)
            methodName = resolveDefaultMethodName();

        try {
            Method method = getClass().getMethod(methodName);

            Annotation[] annotations = method.getAnnotations();

            for (int i = 0; i < annotations.length; i++) {
                Annotation annotation = annotations[i];

                if (annotation.annotationType().getSimpleName().equals("Transactional"))
                    transactional = true;
            }

            if (transactional) {
                if (!jpa().getTransaction().isActive())
                    jpa().getTransaction().begin();
                else
                    inlineCall = true;
            }

            method.invoke(this);

            if (transactional && !inlineCall) {
                jpa().flush();
                jpa().getTransaction().commit();
            }
        } catch (Throwable t) {
            if (transactional && jpa().getTransaction().isActive() && !inlineCall)
                jpa().getTransaction().rollback();

            if (t.getClass().getSimpleName().equals("InvocationTargetException"))
                t = t.getCause();

            if (!inlineCall) {
                response.reset();
                write(errorHandler(t));
            } else {
                System.out.println("PPIPIUPIUPIU");
                throw new RuntimeException(t);
            }

        } finally {
            if (!inlineCall)
                jpa().close();

            try {
                if (!testMode && outputStream != null && !inlineCall)
                    outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            inlineCall = false;
        }
    }

    private String resolveDefaultMethodName() {
        Method[] methods = getClass().getMethods();

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];

            Annotation[] annotations = method.getAnnotations();

            for (int j = 0; j < annotations.length; j++) {
                Annotation annotation = annotations[j];

                if (annotation.annotationType().getSimpleName().equals("Default"))
                    return method.getName();
            }
        }

        return null;
    }

    protected JsonNode errorHandler(Throwable throwable) {
        ObjectNode error = json().createObjectNode();
        error.put("status", "error");
        error.put("message", throwable.getMessage());
        //error.put("exception", throwable.getClass().getName());

        return error;
    }

    protected void write(JsonNode message) {
        if (inlineCall) {
            return;
        }

        try {
            json().writeValue(getoutOutputStream(), message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    protected <T> T read(Class<T> type) {
        if (input != null)
            return (T)input;

        try {
            int contentLength = request.getContentLength();

            if (contentLength <= 0) {
                T instance = null;

                try {
                    instance = type.newInstance();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }

                Method[] methods = type.getDeclaredMethods();

                for (int i = 0; i < methods.length; i++) {
                    Method method = methods[i];

                    if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
                        String convertedName = method.getName().substring(3, method.getName().length());
                        convertedName = Character.toLowerCase(convertedName.charAt(0)) + convertedName.substring(1, convertedName.length());

                        String value = request.getParameter(convertedName);

                        if (value == null)
                            continue;

                        Object convertedValue = null;

                        Class fieldType = method.getParameterTypes()[0];

                        if (fieldType.isAssignableFrom(String.class))
                            convertedValue = value;
                        else if (fieldType.isAssignableFrom(Integer.class))
                            convertedValue = Integer.parseInt(value);
                        else if (fieldType.isAssignableFrom(Double.class))
                            convertedValue = Double.parseDouble(value);

                        try {
                            method.invoke(instance, convertedValue);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }

                    }
                }

                return instance;
            }
            InputStream inputStream = request.getInputStream();
            return json().readValue(inputStream, type);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    protected ObjectMapper json() {
        if (mapper == null)
            mapper = new ObjectMapper();

        return mapper;
    }


    protected ObjectNode newMessage() {
        return json().createObjectNode();
    }


    protected OutputStream getoutOutputStream() throws IOException {
        if (outputStream == null) {
        	response.setContentType("application/json; charset=UTF-8");
        	response.setCharacterEncoding("UTF-8");
        	this.outputStream = response.getOutputStream();
        }
        return outputStream;
    }

    protected String queryParameter(String name) {
        return request.getParameter(name);
    }

    protected String cookie(String name) {
        Cookie[] cookies = request.getCookies();

        for (int i = 0; i < cookies.length; i++) {
            Cookie cookie = cookies[i];

            if (cookie.getName().equals(name))
                return cookie.getValue();
        }

        return null;
    }

    protected String base64Encode(String input) {
        return new String(Base64.encodeBase64URLSafe(input.getBytes()));
    }

    protected String base64Encode(byte[] input) {
        return new String(Base64.encodeBase64URLSafe(input));
    }

    protected String base64Decode(String input) {
        return new String(Base64.decodeBase64(input.getBytes()));
    }

    protected class JpaParam {
        String name;
        Object value;

        public JpaParam(String name, Object value) {
            this.name = name;
            this.value = value;
        }
    }

    protected void error(String message) {
        if (inlineCall)
            throw new RuntimeException(message);

        ObjectNode msg = newMessage();
        msg.put("status", "error");
        msg.put("message", message);

        write(msg);
    }


    protected String ipAddress() {
        String ipAddressFromRequest = request.getRemoteAddr();

        String xForwardedFor = request.getHeader("X-Forwarded-For");

        if (xForwardedFor == null)
            return ipAddressFromRequest;

        return xForwardedFor.split("\\,")[0];
    }

    protected void assertSecureChannel() {
        if (!isTestMode()) {
            StringBuffer sb = request.getRequestURL();

            if (sb == null)
                throw new IllegalAccessError("Insecure request received");

            String url = sb.toString();

            if (!url.startsWith("https")) {
                String xForwardedProto = request.getHeader("X-Forwarded-Proto");

                if (xForwardedProto == null || !xForwardedProto.equalsIgnoreCase("https"))
                    throw new IllegalAccessError("Insecure request received.");
            }
        }
    }
}

